Creating in-process DLL components in Visual Basic isn't significantly different from creating out-of-process components, so the majority of the techniques described in the preceding section, "Creating an ActiveX EXE Server," are also valid for ActiveX DLL components. In this section, I'll focus on the few differences between the two types of components.
CAUTION
If you haven't done it already, download the most recent Service Pack for Visual Basic. Although the Service Pack doesn't add any new features to the language, it fixes a number of serious bugs that occurred with ActiveX DLL components—in particular, those that occurred when the application was using more than seven or eight in-process servers.
In-process components can be created from the Project Properties dialog box by turning a class-based Standard EXE project into an ActiveX DLL project, much as you do with out-of-process components. Alternatively, you can create a new ActiveX DLL project from the Project Gallery dialog box that appears when you issue the New Project command from the File menu.
The main difference between creating out-of-process and in-process components is that the latter ones can be built in the same instance of the IDE as their client. Visual Basic 5 and 6 development environments support the concept of project groups and can host multiple projects in the same instance. To create a project group, you first load or create a project as usual, and then you issue the Add Project command from the File menu to create additional projects or you load existing projects from disk. This ability lets you create a project group made up by one standard EXE and one or more ActiveX DLLs so that you can test one or more in-process components at the same time. You can also save the project group in a file with a .vbg extension so that you can quickly reload all your projects with one Open menu command.
When you issue the Run command, the project that has been marked as the Startup project (see Figure 16-13) begins its execution. This is usually the standard EXE project that works as the client application and that later instantiates one or more objects from the ActiveX DLL projects. You don't need to explicitly run ActiveX DLL projects (as you do with out-of-process components running in separate instances of the Visual Basic IDE), but you still have to add a reference to the DLL in the References dialog box of the standard EXE project.
Figure 16-13. You can make a project the Startup project by right-clicking on it in the Project window.
Be aware that a few commands in the IDE implicitly refer to the current project—that is, the project being highlighted in the Project properties. For example, the contents of the References dialog box is different depending on which project is highlighted, and the Project Properties dialog box lets you see and modify only the attributes of the current project. When the current project is the standard EXE, the Object Browser shows only the Public classes and members of another ActiveX DLL project and doesn't allow you to change the member's attributes. To display all private members or modify the attributes and the descriptions of the DLL's methods and properties, you must make that ActiveX DLL the active project.
Running the DLL in the same environment as its client isn't a limitation because an ActiveX DLL can only have one client. It's loaded in the client's address space and therefore can't be shared with other applications. If two distinct client applications request objects from the same in-process component, COM instantiates two different DLLs, each one in the address space of the client that made the request. For this reason, using an ActiveX DLL is much simpler than using an ActiveX EXE component; the component serves only one client and therefore all requests can be immediately fulfilled. Client applications don't need to account for timeout conditions.
An ActiveX DLL project can't contain SingleUse or GlobalSingleUse classes. The reason for this is that such a DLL runs in the same process as its client and doesn't have a process of its own. So COM can't create a new process for the DLL when the client creates a second object from the component.
ActiveX DLL components can't do everything. In most cases, their limitations are caused by their in-process nature and aren't dictated by Visual Basic.
You deal with errors in in-process components as you do within ActiveX EXE servers. In a sense, however, error handling inside in-process components is even more important because any fatal error in the server also terminates the client and vice versa because the two are actually the same process.
ActiveX DLLs can show their own forms, as out-of-process components do. Interestingly, a form coming from an in-process component is automatically placed in front of forms from its client application, so you don't need to resort to the SetForegroundWindow API function to achieve the right behavior. Depending on the client's capabilities, however, an in-process component might not be able to display nonmodal forms. For example, programs written in Visual Basic 5 or 6, all the applications in the Microsoft Office 97 suite (or later versions), and all the third-party applications that have licensed the VBA language support nonmodal forms displayed by in-process components. On the other hand, programs written with Visual Basic 4 and all the applications found in previous versions of Microsoft Office raise an error 369 when a DLL component tries to display a nonmodal form.
Visual Basic enables you to test whether a client supports nonmodal forms through the App.NonModalAllowed read-only property. Microsoft suggests that you test this property before showing a nonmodal form from within a component, and degrade gracefully by showing a modal form if necessary:
If App.NonModalAllowed Then frmChart.Show Else frmChart.Show vbModal End If |
If you consider that the vbModal constant is 1 and that the App.NonModalAllowed returns 0 or _1, you can do everything in just one statement:
frmChart.Show (1 + App.NonModalAllowed) |
Unfortunately, you can't test this feature without compiling the component into an actual ActiveX DLL because the App.NonModalAllowed property always returns True when the program runs in the Visual Basic environment.
The rules that state when an in-process component is terminated are different from those you've seen for out-of-process components. The main difference is that an in-process component always follows the destiny of its client: When the client terminates, the component also terminates even if it has visible forms. When the client is still executing, an in-process component is terminated if all of the following conditions are true:
The fact that an in-process server is kept alive also by internal references to its own objects raises a nontrivial problem. For example, if the component includes two objects that have references to each other, the component will never be shut down when the client releases all the references to it. In other words, circular references can keep an in-process component alive until the client terminates. There's no simple way to solve this problem, and it's up to the programmer to avoid creating circular references. (For more information about the circular reference problem, see Chapter 7.)
Another important detail in the behavior of in-process components might disorient many programmers. While ActiveX EXE components are terminated as soon as the client releases all the references to them (provided that all the other necessary conditions are met), in-process components aren't released immediately. In general, Visual Basic keeps them alive for a couple of minutes (the exact delay may vary, though) so that if another request comes from the client, COM doesn't have to reload the server. If this timeout expires, the DLL is silently unloaded and its memory is released. A new request coming from the client at this point will take a little more time because COM has to reload the component.
CAUTION
Only references to public objects can keep a component alive. Even if an in-process DLL manages to pass its client a pointer to a private object (for example, by using an As Object argument or return value), this reference won't keep the component alive. So if the client releases all the references to the component's public objects, after some time the component will be unloaded. The variable owned by the client becomes invalid and crashes the application as soon as it's used in any way. For this reason, a component should never pass a private object to its client.
Calls to an in-process component's methods or properties are served immediately, even if the component is currently serving another request. This differs from how out-of-process components behave and raises a number of issues that you must account for:
As you see, both problems are caused by the fact that the client calls the component while it's serving a previous request. This can happen if the component executes a DoEvents command that lets the client become active again, if the component raises an event in its client application, or if the client calls the component from within a Timer control's Timer event procedure. If you avoid these circumstances, you should never experienced reentrancy problems. Alternatively, you can implement a semaphore, a global variable in the client that keeps track of when it's safe to call the component.
You should be aware of a few more features of the behavior of an in-process component; these are important when you convert some classes from a standard Visual Basic application into an ActiveX DLL component. For example, a number of objects and keywords refer to the component's environment, not the client's:
A few other features don't work as they normally do:
ActiveX DLL servers offer a great way to reuse common forms and dialog boxes. As you know, form modules can't be Public, so they can't be visible from outside the project. But you can create a class that wraps around a form and exposes the same interface and then make the class Public so that you can create it from other applications. Existing applications need minor or no modifications at all to use the component instead of the form. The only requirement for doing this, in fact, is that an application never directly references controls on the form, which is something that you should not do anyway to preserve the form's encapsulation. (For more information about this issue, see Chapter 9.)
Say that you have created a frmLogin form that accepts a user's name and password and validates them. In this simple example, the only valid user name is francesco, which corresponds to the balena password. The form has two TextBox controls, named txtUsername and txtPassword, and one cmdOK CommandButton control. The form also exposes one event, WrongPassword, that's raised when the user clicks on the OK button and the user name or the password is invalid. This event can be trapped by the client code to show a message box to the user, as you can see in Figure 16-14. This is the complete source code of the form module:
Event WrongPassword(Password As String) Public UserName As String Public Password As String Private Sub cmdOK_Click() ' Validate the password. If LCase$(txtUserName= "francesco" And LCase$(txtPassword) = _ "balena" Then Unload Me Else RaiseEvent WrongPassword(txtPassword) End If End Sub Private Sub Form_Load() txtUserName = UserName ' Load properties into fields. txtPassword = Password End Sub Private Sub Form_Unload(Cancel As Integer) UserName = txtUserName ' Load field values into properties. Password = txtPassword End Sub |
You can use this form as if it were a class, without ever directly referencing the controls on its surface. This is the code of the main form in the demonstration program on the companion CD:
Dim WithEvents frmLogin As frmLogin Private Sub Command1_Click() Set frmLogin = New frmLogin frmLogin.Show vbModal MsgBox "User " & frmLogin.UserName & " logged in", vbInformation End Sub Private Sub frmLogin_WrongPassword(password As String) MsgBox "Wrong Password" End Sub |
Figure 16-14. An in-process component can conveniently encapsulate a reusable form and expose its events to client applications.
Because the form can be used without accessing its controls, you can now wrap a CLogin class module around the frmLogin form and encapsulate both the class and the form modules in a LoginSvr DLL that exposes the form's functionality to the outside. The source code of the CLogin class is shown below.
Event WrongPassword(Password As String) Private WithEvents frmLogin As frmLogin Private Sub Class_Initialize() Set frmLogin = New frmLogin End Sub Public Property Get UserName() As String UserName = frmLogin.UserName End Property Public Property Let UserName(ByVal newValue As String) frmLogin.UserName = newValue End Property Public Property Get Password() As String Password = frmLogin.Password End Property Public Property Let Password(ByVal newValue As String) frmLogin.Password = newValue End Property Sub Show(Optional mode As Integer) frmLogin.Show mode End Sub Private Sub frmLogin_WrongPassword(Password As String) RaiseEvent WrongPassword(Password) End Sub |
As you see, the UserName and Password properties and the Show method of the class simply delegate to the form's members with the same name. Moreover, the class traps the WrongPassword event coming from the form and raises an event with the same name in its client application. In short, the class exposes exactly the same interface as the original form. If you set the class's Instancing attribute to 5-MultiUse, the class (and hence the form) can be reused by any client application. You only have to change a couple of lines of code in the original client application to have it work with the CLogin class instead of the frmLogin class. (The modified code is in boldface.)
Dim WithEvents frmLogin As CLogin Private Sub Command1_Click() Set frmLogin = New CLogin frmLogin.Show vbModal MsgBox "User " & frmLogin.UserName & " logged in", vbInformation End Sub Private Sub frmLogin_WrongPassword(password As String) MsgBox "Wrong Password" End Sub |
You can use this technique to create both modal and modeless reusable forms. You can't, however, use forms embedded in a DLL as MDI child forms in an MDI application.
You can improve the performance of your ActiveX DLL servers the following ways.
Because the DLL runs in the same address space as its client, COM doesn't need to marshal data being passed from the client to the component and back. Actually, the role of COM with in-process components is much simpler than with out-of-process servers because COM only has to make sure that the DLL is correctly instantiated when the client requests an object from it. From that point onward, the client communicates directly with the component. COM will become active again only to ensure that the DLL is released when the client doesn't need it any longer.
The process switch that occurs any time a client calls an out-of-process component considerably slows down ActiveX EXE components. For example, calling an empty procedure without any arguments in an out-of-process component is about 500 times slower than calling an empty procedure in an in-process DLL! Surprisingly, a method in a DLL takes more or less the same time as a method in a Private class of the client application, which proves that the overhead for a call to an in-process component is negligible.
The absence of marshaling also suggests that the optimization rules for passing data to an in-process DLL might differ from those you should follow when working with out-of-process EXE servers. For example, there's no significant difference between passing a number to an in-process procedure using ByRef or ByVal. But you'd better pass longer strings by reference rather than by value: I built a simple benchmark program (which you can find on the companion CD) that compares the performance of in-process and out-of-process servers. I found that passing a 1000-character string by value can be 10 times slower than passing it by reference. And the longer the string is, the slower passing it by value is.
If you have multiple clients that are using the same in-process component at the same time, a separate instance of the DLL is loaded in each client's address space. This might result in a waste of memory unless you take some precautions.
Thanks to advanced features of the Windows virtual memory subsystem, you can load the same DLL in distinct address spaces without using more memory than required by a single instance of the DLL. More precisely, multiple client applications can share the same image of the DLL loaded from disk. This is possible, however, only if all the instances of the DLL are loaded at the same address in the memory space of the different processes and if this address coincides with the DLL's base address.
The base address of a DLL is the default address at which Windows tries to load the DLL within the address space of its clients. If the attempt is successful, Windows can load the DLL quickly because it just has to reserve an area of memory and load the contents of the DLL file there. On the other hand, if Windows can't load the DLL at its base address (most likely because that area has been allocated to another DLL), Windows has to find a free block in memory that's large enough to contain the DLL, and then it must relocate the DLL's code. The relocation process changes the addresses of jump and call instructions in the DLL's binary code to account for the different load address of the DLL.
Summarizing, it's far preferable that a DLL be loaded at its base address for two reasons:
Visual Basic lets you select the base address for an in-process DLL server in the Compile tab of the Project Properties dialog box, as you see in Figure 16-15. The default value for this address is H11000000, but I strongly advise you to modify it before you compile the final version of your component. If you don't, your DLL base address will conflict with other DLLs written in Visual Basic. Only one DLL can win, and all the others will be relocated.
Figure 16-15. You can improve the performance of an ActiveX DLL by changing its base address.
Fortunately, other languages have different default values. For example, DLLs written in Microsoft Visual C++ default to address H10000000, so even if their programmers didn't modify this default setting, these DLLs won't conflict with those authored in Visual Basic.
When you're deciding which base address you should specify for a Visual Basic DLL, take the following points into account:
For example, a base address greater than 1 GB (&H40000000) accommodates the largest client application that you can ever build and still leaves one gigabyte for your DLLs. Even after accounting for the 64-KB page size, this leaves you with 16,384 different values to choose from when assigning a base address to your DLL.
ActiveX DLL servers are very useful to augment the functionality of an application through so-called Satellite DLLs. To understand why satellite DLLs are so advantageous, let's see first what resource files are.
Resource files are files, usually with the .res extension, that can contain strings, images, and binary data used by an application. You create resource files in two steps. First of all, you prepare a text file (usually with the .rc extension) that contains the description of the contents of the resource file. This text file must follow a well-defined syntax. For example, here's a fragment of an RC file that defines two strings and one bitmap:
STRINGTABLE BEGIN 1001 "Welcome to the Imaging application" 1002 "Do you want to quit now?" END 2001 BITMAP c:\windows\clouds.bmp |
In the second step, you compile the .rc file into a .res file, using the Rc.exe resource compiler with the /r switch on the command line. (This utility comes with Visual Basic.)
RC /r TEST.RC |
At the end of the compilation, you obtain a .res file with the same base name as the .rc file (test.res in this example). You can now load this new file into the Visual Basic environment using the Add File command in the Project menu.
NOTE
Visual Basic 6 greatly simplifies the resource file creation and compilation phases using a new add-in, the VB Resource Editor, shown in Figure 1616. This add-in also supports multiple string tables, which let your application conform to the user's language automatically. A Visual Basic 5 version of this add-in is also available for downloading from Microsoft's Web site.
Figure 16-16. The VB Resource Editor can create resource files with bitmaps, icons, sounds, and multiple string tables.
After you create a .res file, your code can reference the resources it contains using the LoadResString, LoadResPicture, and LoadResData functions, as the following example shows:
' Print a welcome message. Print LoadResString(1001) ' Load an image into a PictureBox control. Picture1.Picture = LoadResString(2001, vbResBitmap) |
Resource files are a great choice when you're creating an application that must be localized for other countries. The source code is completely independent of all the strings and pictures used by the program, and when you want to create a new version of the application for a different country you only have to prepare a different resource file. To learn more about resource files, have a look at the ATM.VBP sample project that comes with Visual Basic 6.
Even with the help of the VB Resource Editor add-in, however, working with resource files is rather cumbersome for the following reasons:
Both these problems can be solved using satellite DLLs.
The concept on which satellite DLLs are based is simple: Instead of loading strings and other resources from resource files, you load them from an ActiveX DLL. The trick is that you instantiate an object from the DLL using CreateObject instead of the New operator, and therefore you can select the DLL you load at run time. This approach lets you ship a DLL to your customers even after they've installed the main application, so you can effectively add support for new languages as soon as you prepare new DLLs. The user can switch from one DLL to another at run time—for example, with a menu command.
I've prepared a demonstration application that uses satellite DLLs to create a simple database program whose interface adapts itself to the nationality of the user. (See Figure 16-17.) When the application starts, it selects the DLL that matches the version of the Windows operating system in use or defaults to the English version if no DLL for the current language is found.
Figure 16-17. A multiple-language application that uses satellite DLLs to support both English and Italian.
A satellite DLL that exports strings, bitmaps, and binary data must expose at least three functions. To make satellite DLLs look like resource files, you can name them LoadResString, LoadResPicture, and LoadResData. Here's a portion of the source code of the DLL provided with the sample application:
' The Resources class module in the Application000 project Enum ResStringID rsDataError = 1 rsRecord rsPublishers ' (Other enumerated values omitted...) End Enum Enum ResPictureID rpFlag = 1 End Enum Enum ResDataID rdDummy = 1 ' This is a necessary placeholder. End Enum Function LoadResString(ByVal ID As ResStringID) As String Select Case ID Case rsPublishers: LoadResString = "Publishers" Case rsClose: LoadResString = "&Close" Case rsRefresh: LoadResString = "&Refresh" ' (Other Case clauses omitted...) End Select End Function Function LoadResPicture(ByVal ID As ResPictureID, _ Optional Format As Long) As IPictureDisp ' Loads images from the frmResources form Select Case ID Case rpFlag: Set LoadResPicture = _ frmResources000.imgFlag.Picture End Select End Function Function LoadResData(ByVal ID As ResDataID, _ Optional Format As Long) As Variant ' Not used in this sample program End Function |
This particular DLL includes only one bitmap and doesn't include any binary data. For simplicity's sake, the bitmap has been loaded at design time in an Image control on the frmResources form. This form is never displayed and works only as a container for the bitmap. You can use this approach also for storing icons and cursors. If you need to store other types of binary data, however, you can use a resource file. In this instance, however, each satellite DLL has its own resource file.
The trick in using satellite DLLs is to use the primary DLL (namely, the DLL that provides the resources for the default language—English in this example) as the interface that DLLs for other languages must implement. Let's see how the Italian DLL is implemented:
' The Resources class module in the Application410 project Implements MyApplication000.Resources Private Function Resources_LoadResString(ByVal ID As _ MyApplication000.ResStringID) As String Dim res As String Select Case ID Case rsPublishers: res = "Editori" Case rsClose: res = "&Chiudi" Case rsRefresh: res = "&Aggiorna" ' (Other Case clauses omitted...) End Select Resources_LoadResString = res End Function Private Function Resources_LoadResPicture(ByVal ID As _ MyApplication000.ResPictureID, Optional Format As Long) _ As IPictureDisp Select Case ID Case rpFlag: Set Resources_LoadResPicture = _ frmResources410.imgFlag.Picture End Select End Function Private Function Resources_LoadResData(ByVal ID As _ MyApplication000.ResDataID, Optional Format As Long) As Variant ' Not used in this program End Function |
Notice that this class has no members in its primary interface. The Italian DLL is stored in a project named MyApplication410.vbp, whereas the default DLL is stored in a project named MyApplication000.vbp. The reason for this naming scheme will be clear in a moment.
Let's have a look at how a client application can leverage the power and flexibility of satellite DLLs to automatically adapt itself to the locale of users while still providing them with the capability to switch to a different language at run time. The secret is in an API function, GetUserDefaultLangID, which returns the locale identifier of the current interactive user. The client application uses this value to build the name of the DLL and then passes it to the CreateObject function, as the code below demonstrates.
' The main BAS module in the client application Declare Function GetUserDefaultLangID Lib "kernel32" () As Long Public rs As New MyApplication000.Resources Sub Main() InitLanguage ' Load the satellite DLL. frmPublishers.Show ' Show the startup form. End Sub ' Load the satellite DLL that corresponds to the current user's locale. Sub InitLanguage() Dim LangId As Long, ProgID As String ' Get the default language. LangId = GetUserDefaultLangID() ' Build the complete class name. ProgID = App.EXEName & Hex$(LangId) & ".Resources" ' Try to create the object, but ignore errors. If this statement ' fails, the RS variable will point to the default DLL (English). On Error Resume Next Set rs = CreateObject(ProgID) End Sub |
The key to this technique is in the InitLanguage procedure, where the application dynamically builds the name of the DLL that would provide the resources for the current locale. For example, when executed under an Italian version of Windows, the GetUserDefaultLangID API function returns the value 1040, or &H410.
You can create satellite DLLs for other languages and ship them to your foreign customers. This approach always works perfectly, provided that you assign a project a name like MyApplicationXXX, where XXX is the hexadecimal locale identifier. (For a list of locale identifiers, see the Windows SDK documentation.) The first portion of the project name must match the client application's project name (MyApplication, in this example), but you can devise other effective ways to dynamically build the DLL's name.
If the CreateObject function fails, the rs variable won't be initialized in the InitLanguage procedure, but because it's declared as an auto-instancing variable it automatically instantiates the default MyApplication000.Resource component. The key point here is that all the satellite DLLs for this particular application implement the same interface, so the rs variable can hold a reference to any satellite using early binding. See how the rs variable is used within a form of the client application:
Private Sub Form_Load() LoadStrings End Sub Private Sub LoadStrings() Me.Caption = rs.LoadResString(rsPublishers) cmdClose.Caption = rs.LoadResString(rsClose) cmdRefresh.Caption = rs.LoadResString(rsRefresh) ' (Other string assignments omitted...) Set imgFlag.Picture = rs.LoadResPicture(rpFlag) End Sub |
Because the MyApplication000.Resource class declares enumerated constants for all the strings and other resources in the satellite DLL, you can use IntelliSense to speed up the development phase and produce a more readable and self-documenting code at the same time.